/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package axil.engine.access.discovery;

import axil.api.Axil;
import axil.api.Axil.Case;
import axil.api.Context;
import axil.api.access.AccessibleField;
import axil.api.access.AccessibleType;
import axil.api.access.Accessor;
import axil.engine.access.FieldAccessor;
import axil.engine.access.MethodAccessor;
import axil.engine.access.transform.Transform;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import static axil.framework.Functions.*;


/**
 * A field discovery mechanism based upon reflectively examining a class for
 * fields and methods according to a policy stated for the class.
 */
class ReflectiveDiscovery extends Discovery {
	private String type;
	private Map<String, Accessor> map;


	ReflectiveDiscovery(Class type, AccessibleType access) {
		this.type = type.getName();
		this.map = new HashMap<String, Accessor>();
		switch (access.mechanism()) {
		case EXPLICIT:
			/**
			 * Add only the fields that are explicitly specified via the
			 * @AccessibleField annotation on a field or method. All others
			 * will be ignored.
			 */
			explicit(type, access);
			break;

		case ANY:
			/*
			 * Automatically add any non-transient field discovered in the
			 * object. This includes any fields that are public, private or
			 * protected.
			 */
			fields(type, access);
			break;

		case PUBLIC:
			/**
			 * Automatically add every method discovered that follows Java
			 * Bean-style programming conventions. That is, any public
			 * getX() or isX() method will be detected.
			 */
			beans(type, access);
			break;

		default:
			throw unreachable();
		}
	}


	/**
	 * Add only the fields that are explicitly specified via the
	 * @AccessibleField annotation on a field or method. All others will be
	 * ignored.
	 */
	private void explicit(Class type, AccessibleType access) {
		for (Field f : type.getDeclaredFields()) {
			AccessibleField af = f.getAnnotation(AccessibleField.class);
			if (af != null) {
				String name = af.name();
				if (empty(name)) {
					name = Transform.transform(Axil.instance(), f.getName());
				}
				f.setAccessible(true);
				Accessor a = new FieldAccessor(f);
				put(name, a);
				aliases(af, a);
			}
		}

		for (Method m : type.getDeclaredMethods()) {
			AccessibleField af = m.getAnnotation(AccessibleField.class);
			if (af != null) {
				String name = af.name();
				if (empty(name)) {
					name = name.substring(name.startsWith("is") ? 2 : 3);
					name = Transform.transform(Axil.instance(), name);
				}
				m.setAccessible(true);
				Accessor a = new MethodAccessor(m);
				put(name, a);
				aliases(af, a);
			}

		}
	}


	private void put(String name, Accessor accessor) {
		if (map.containsKey(name)) {
			throw abort("The type has more than one field resulting in the " +
			            "same identifier",
			            nv("type", type),
			            nv("duplicate identifier", name));
		}
		map.put(name, accessor);
	}


	private void aliases(AccessibleField af, Accessor accessor) {
		String aliases = af.aliases();
		if (! empty(aliases)) {
			for (String alias : aliases.split("\\,")) {
				put(alias, accessor);
			}
		}
	}


	/*
	 * Automatically add any non-transient field discovered in the object. This
	 * includes any fields that are public, private or protected.
	 */
	private void fields(Class type, AccessibleType access) {
		for (Field f : type.getFields()) {
			if (! Modifier.isTransient(f.getModifiers())) {
				f.setAccessible(true);
				put(Transform.transform(Axil.instance(),
				                        f.getName()),new FieldAccessor(f));
			}
		}
	}


	/**
	 * Automatically add every method discovered that follows Java Bean-style
	 * programming conventions. That is, any public getX() or isX() method will
	 * be detected.
	 */
	private void beans(Class type, AccessibleType access) {
		for (Method m : type.getMethods()) {
			String name = m.getName();
			if (Modifier.isPublic(m.getModifiers()) &&
			        (m.getParameterTypes().length == 0) && bean(name)) {
				name = name.substring(name.startsWith("is") ? 2 : 3);
				put(Transform.transform(Axil.instance(), name),
				    new MethodAccessor(m));
			}
		}
	}


	/**
	 * Tell if this method name is a Java Beans-style name. We recognize isX()
	 * as a match, but not something like isolate().
	 */
	private boolean bean(String name) {
		final int size = name.length();
		if (name.startsWith("is")) {
			return (size > 2) && Character.isUpperCase(name.charAt(2));
		}
		if (name.startsWith("get")) {
			return (size > 3) && Character.isUpperCase(name.charAt(3));
		}
		return false;
	}


	public Accessor accessor(String field) {
		Axil axil = Axil.instance();
		Accessor a = map.get(field);
		if (a == null) {
			if (axil.sensitivity() == Case.INSENSITIVE) {
				a = map.get(field.toUpperCase());
			}
		}
		return a;
	}


	/**
	 * Get an accessor for the given field residing within the parent object.
	 *
	 * @param context
	 * 	The context being used for the access.
	 *
	 * @param parent
	 * 	The parent object. This is the object that is expected to contain the
	 * 	given field. The parent object is never null.
	 *
	 * @param field
	 * 	The name of the field as it appears in the Axil script. Note that Axil
	 * 	supports a highly flexible identifier mechanism, allowing spaces and
	 * 	punctuation in identifiers. It is the responsibility of the host
	 * 	application to define how identifiers from Axil are mapped to actual
	 * 	object fields.
	 *
	 * @return
	 * 	Returns a non-null accessor object. If the field cannot be found, then
	 * 	an exception is thrown.
	 */
	public Accessor accessor(Context context, Object parent, String field) {
		return accessor(field);
	}
}
